﻿using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace Library.Winform.Hooks {
  /// <summary>
  /// Captures global mouse events
  /// </summary>
  public class MouseHook : GlobalHook {
   
    private static readonly object EventMouseDown = new object();
    private static readonly object EventMouseUp = new object();
    private static readonly object EventMouseMove = new object();
    private static readonly object EventMouseWheel = new object();
    private static readonly object EventClick = new object();
    private static readonly object EventDoubleClick = new object();

    #region MouseEventType Enum

    private enum MouseEventType {
      None,
      MouseDown,
      MouseUp,
      DoubleClick,
      MouseWheel,
      MouseMove
    }

    #endregion

    #region Events

    public event MouseEventHandler MouseDown {
      add {
        Events.AddHandler(EventMouseDown, value);
      }
      remove {
        Events.RemoveHandler(EventMouseDown, value);
      }
    }
    public event MouseEventHandler MouseUp {
      add {
        Events.AddHandler(EventMouseUp, value);
      }
      remove {
        Events.RemoveHandler(EventMouseUp, value);
      }
    }
    public event MouseEventHandler MouseMove {
      add {
        Events.AddHandler(EventMouseMove, value);
      }
      remove {
        Events.RemoveHandler(EventMouseMove, value);
      }
    }
    public event MouseEventHandler MouseWheel {
      add {
        Events.AddHandler(EventMouseWheel, value);
      }
      remove {
        Events.RemoveHandler(EventMouseWheel, value);
      }
    }
    public event EventHandler Click {
      add {
        Events.AddHandler(EventClick, value);
      }
      remove {
        Events.RemoveHandler(EventClick, value);
      }
    }
    public event EventHandler DoubleClick {
      add {
        Events.AddHandler(EventDoubleClick, value);
      }
      remove {
        Events.RemoveHandler(EventDoubleClick, value);
      }
    }

    #endregion

    #region Constructor

    public MouseHook() {
      HookType = WhMouseLl;
    }

    #endregion

    #region Methods

    protected override int HookCallbackProcedure(int nCode, int wParam, IntPtr lParam) {
      var mouseDownHandler = (MouseEventHandler)Events[EventMouseDown];
      var mouseUpHandler = (MouseEventHandler)Events[EventMouseUp];
      var mouseMoveHandler = (MouseEventHandler)Events[EventMouseMove];
      var mouseWheelHandler = (MouseEventHandler)Events[EventMouseWheel];
      var clickHandler = (EventHandler)Events[EventClick];
      var doubleClickHandler = (EventHandler)Events[EventDoubleClick];
      if (nCode > -1 && (mouseDownHandler != null || mouseUpHandler != null || mouseMoveHandler != null || mouseWheelHandler != null || clickHandler != null || doubleClickHandler != null)) {
        var mouseHookStruct =
          (MouseLlHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseLlHookStruct));

        MouseButtons button = GetButton(wParam);
        MouseEventType eventType = GetEventType(wParam);

        var e = new MouseEventArgs(
          button,
          (eventType == MouseEventType.DoubleClick ? 2 : 1),
          mouseHookStruct.pt.x,
          mouseHookStruct.pt.y,
          (eventType == MouseEventType.MouseWheel ? (short)((mouseHookStruct.mouseData >> 16) & 0xffff) : 0));

        // Prevent multiple Right Click events (this probably happens for popup menus)
        if (button == MouseButtons.Right && mouseHookStruct.flags != 0) {
          eventType = MouseEventType.None;
        }

        switch (eventType) {
          case MouseEventType.MouseDown:
            if (mouseDownHandler != null) {
              mouseDownHandler(this, e);
            }
            break;
          case MouseEventType.MouseUp:
            if (clickHandler != null) {
              clickHandler(this, new EventArgs());
            }
            if (mouseUpHandler != null) {
              mouseUpHandler(this, e);
            }
            break;
          case MouseEventType.DoubleClick:
            if (doubleClickHandler != null) {
              doubleClickHandler(this, new EventArgs());
            }
            break;
          case MouseEventType.MouseWheel:
            if (mouseWheelHandler != null) {
              mouseWheelHandler(this, e);
            }
            break;
          case MouseEventType.MouseMove:
            if (mouseMoveHandler != null) {
              mouseMoveHandler(this, e);
            }
            break;
          default:
            break;
        }
      }

      return CallNextHookEx(HandleToHook, nCode, wParam, lParam);
    }

    private static MouseButtons GetButton(Int32 wParam) {
      switch (wParam) {
        case WmLbuttondown:
        case WmLbuttonup:
        case WmLbuttondblclk:
          return MouseButtons.Left;
        case WmRbuttondown:
        case WmRbuttonup:
        case WmRbuttondblclk:
          return MouseButtons.Right;
        case WmMbuttondown:
        case WmMbuttonup:
        case WmMbuttondblclk:
          return MouseButtons.Middle;
        default:
          return MouseButtons.None;
      }
    }

    private static MouseEventType GetEventType(Int32 wParam) {
      switch (wParam) {
        case WmLbuttondown:
        case WmRbuttondown:
        case WmMbuttondown:
          return MouseEventType.MouseDown;
        case WmLbuttonup:
        case WmRbuttonup:
        case WmMbuttonup:
          return MouseEventType.MouseUp;
        case WmLbuttondblclk:
        case WmRbuttondblclk:
        case WmMbuttondblclk:
          return MouseEventType.DoubleClick;
        case WmMousewheel:
          return MouseEventType.MouseWheel;
        case WmMousemove:
          return MouseEventType.MouseMove;
        default:
          return MouseEventType.None;
      }
    }

    #endregion
  }
}